﻿using Geometry;
using Drawing;
using System.Linq;
using System.Collections.Generic;

namespace UserInterface.Primitives
{
    public class Grid : VisualContainer
    {
        public static readonly Property<int> Row = new Property<int>(0);
        public static readonly Property<int> Column = new Property<int>(0);
        public static readonly Property<int> RowSpan = new Property<int>(1);
        public static readonly Property<int> ColumnSpan = new Property<int>(1);

        public Grid()
        {
            this.Rows = new GridField();
            this.Columns = new GridField();
        }

        public GridField Rows { get; private set; }
        public GridField Columns { get; private set; }
        private Size FieldsDesiredSize
        {
            get
            {
                return new Size(this.Columns.DesiredLength, this.Rows.DesiredLength);
            }
        }

        protected override Size MeasureContent(Size upToSize, IDrawingContext context)
        {
            this.Rows.Reset();
            this.Columns.Reset();

            var groups = GroupVisualsForMeasure();

            var infinity = new Size(float.PositiveInfinity, float.PositiveInfinity);

            groups.Auto.ForEach(v => v.Measure(infinity, context));

            this.PushRowSingleBandAutos(groups.Auto);
            this.PushColumnSingleBandAutos(groups.Auto);

            if (groups.RowAuto.Count != 0 && groups.ColumnAuto.Count != 0)
            {
                groups.RowAuto.ForEach(v => v.Measure(infinity, context));
                groups.ColumnAuto.ForEach(v => v.Measure(infinity, context));

                this.PushRowSingleBandAutos(groups.RowAuto);
                this.PushColumnSingleBandAutos(groups.ColumnAuto);

                this.PushRowMultyBandAutos(groups.Auto);
                this.PushColumnMultyBandAutos(groups.Auto);
                this.PushRowMultyBandAutos(groups.RowAuto);
                this.PushColumnMultyBandAutos(groups.ColumnAuto);

                this.Rows.SetAvailableLengthForPercents(upToSize.Height);
                this.Columns.SetAvailableLengthForPercents(upToSize.Width);
            }
            else if (groups.RowAuto.Count != 0)
            {
                this.PushColumnMultyBandAutos(groups.Auto);
                // we need available width for percents here
                this.Columns.SetAvailableLengthForPercents(upToSize.Width);
                groups.RowAuto.ForEach(v => v.Measure(new Size(AvailableWidth(v), float.PositiveInfinity), context));

                this.PushRowSingleBandAutos(groups.RowAuto);
                this.PushRowMultyBandAutos(groups.RowAuto);
                this.Rows.SetAvailableLengthForPercents(upToSize.Height);
            }
            else if (groups.ColumnAuto.Count != 0)
            {
                PushRowMultyBandAutos(groups.Auto);
                // we need available height for percents here
                this.Rows.SetAvailableLengthForPercents(upToSize.Height);
                groups.ColumnAuto.ForEach(v => v.Measure(new Size(float.PositiveInfinity, AvailableHeight(v)), context));

                this.PushColumnSingleBandAutos(groups.ColumnAuto);
                this.PushColumnMultyBandAutos(groups.ColumnAuto);
                this.Columns.SetAvailableLengthForPercents(upToSize.Width);
            }

            var autoSize = this.FieldsDesiredSize;

            this.PushRowPercentItems(groups.ColumnAuto);
            this.PushColumnPercentItems(groups.RowAuto);
            // we need available width for percents here
            groups.NonAuto.ForEach(v => v.Measure(AvailableSize(v), context));

            this.PushRowPercentItems(groups.NonAuto);
            this.PushColumnPercentItems(groups.NonAuto);

            this.Rows.CalculatePercentsDesiredSize();
            this.Columns.CalculatePercentsDesiredSize();

            var percentSize = this.FieldsDesiredSize;

            var desiredSize = Size.Max(autoSize, Size.Min(upToSize, percentSize));
            return desiredSize;
        }

        protected override void ArrangeContent(Size size, IDrawingContext context)
        {
            this.Rows.SetArrangeLengthForPercents(size.Height);
            this.Columns.SetArrangeLengthForPercents(size.Width);
            foreach (var visual in this)
            {
                var rect = this.ArrangeRect(visual);
                visual.Arrange(rect, context);
            }
        }

        private Rect ArrangeRect(Visual visual)
        {
            var horizontalInterval = this.Columns.GetArrangeInterval(Grid.Column.GetValue(visual), Grid.ColumnSpan.GetValue(visual));
            var verticalInterval = this.Rows.GetArrangeInterval(Grid.Row.GetValue(visual), Grid.RowSpan.GetValue(visual));
            var rect = new Rect(horizontalInterval, verticalInterval);
            return rect;
        }

        private Size AvailableSize(Visual visual)
        {
            return new Size(AvailableWidth(visual), AvailableHeight(visual));
        }

        private float AvailableHeight(Visual visual)
        {
            return this.Rows.GetAvailableSpan(Grid.Row.GetValue(visual), Grid.RowSpan.GetValue(visual));
        }

        private float AvailableWidth(Visual visual)
        {
            return this.Columns.GetAvailableSpan(Grid.Column.GetValue(visual), Grid.ColumnSpan.GetValue(visual));
        }

        private void PushRowSingleBandAutos(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals.Where(v => Grid.RowSpan.GetValue(v) == 1))
            {
                this.Rows.PushSingleBandAutoItem(Grid.Row.GetValue(visual), visual.DesiredSize.Height);
            }
        }

        private void PushColumnSingleBandAutos(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals.Where(v => Grid.ColumnSpan.GetValue(v) == 1))
            {
                this.Columns.PushSingleBandAutoItem(Grid.Column.GetValue(visual), visual.DesiredSize.Width);
            }
        }

        private void PushRowMultyBandAutos(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals.Where(v => Grid.RowSpan.GetValue(v) > 1))
            {
                this.Rows.PushSpanningAutoItem(Grid.Row.GetValue(visual), Grid.RowSpan.GetValue(visual), visual.DesiredSize.Height);
            }
        }

        private void PushColumnMultyBandAutos(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals.Where(v => Grid.ColumnSpan.GetValue(v) > 1))
            {
                this.Columns.PushSpanningAutoItem(Grid.Column.GetValue(visual), Grid.ColumnSpan.GetValue(visual), visual.DesiredSize.Width);
            }
        }

        private void PushRowPercentItems(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals)
            {
                this.Rows.PushPercentItem(Grid.Row.GetValue(visual), Grid.RowSpan.GetValue(visual), visual.DesiredSize.Height);
            }
        }

        private void PushColumnPercentItems(IEnumerable<Visual> visuals)
        {
            foreach (var visual in visuals)
            {
                this.Columns.PushPercentItem(Grid.Column.GetValue(visual), Grid.ColumnSpan.GetValue(visual), visual.DesiredSize.Width);
            }
        }

        private MeasureGroups GroupVisualsForMeasure()
        {
            var groups = new MeasureGroups()
            {
                Auto = new List<Visual>(),
                RowAuto = new List<Visual>(),
                ColumnAuto = new List<Visual>(),
                NonAuto = new List<Visual>()
            };
            foreach (var visual in this)
            {
                bool isRowAuto = this.Rows.IsSpanAuto(Grid.Row.GetValue(visual), Grid.RowSpan.GetValue(visual));
                bool isColumnAuto = this.Columns.IsSpanAuto(Grid.Column.GetValue(visual), Grid.ColumnSpan.GetValue(visual));
                var group = (isRowAuto ? (isColumnAuto ? groups.Auto : groups.RowAuto) : (isColumnAuto ? groups.ColumnAuto : groups.NonAuto));
                group.Add(visual);
            }

            return groups;
        }

        private struct MeasureGroups
        {
            public List<Visual> Auto;
            public List<Visual> RowAuto;
            public List<Visual> ColumnAuto;
            public List<Visual> NonAuto;
        }
    }
}
